package model;

import static util.Constants.BOARD_HEIGHT;
import static util.Constants.BOARD_WIDTH;
import static util.Constants.START_SQUARE;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.swing.event.EventListenerList;

import model.events.PlayerEvent;
import model.events.TurnEvent;
import model.events.errors.BadMoveError;
import model.events.errors.EmptyBagError;
import model.events.errors.EndGameError;
import model.events.errors.ErrorEvent;
import model.events.errors.ForbiddenActionError;
import model.iview.IBoard;
import model.iview.IPlayer;
import model.iview.ITile;
import model.listeners.ErrorListener;
import model.listeners.PlayerListener;
import model.listeners.RackListener;
import model.listeners.TurnListener;
import util.Coordinate;
import util.Resources;

/**
 * Cette classe est le modèle principal du jeu de scrabble. Il s'occupe de gérer
 * les règles du jeu, de gérer les joueurs en cours et de décider de la fin du
 * jeu.
 * 
 * @author Lucile Gramusset et Guillaume Poussel
 * 
 */
public class ScrabbleGame {

    private Bag bag = new Bag();
    private Board board = Board.getInstance();
    private int currentPlayer;
    private EventListenerList playerListeners = new EventListenerList();
    private EventListenerList errorListeners = new EventListenerList();
    private List<Player> players = new ArrayList<Player>();
    private EventListenerList turnListeners = new EventListenerList();
    private int wordCount = 0;

    /**
     * Ajoute un joueur au jeu
     * 
     * @param player
     *            Le joueur à ajouter
     */
    public void addPlayer(Player player) {
        players.add(player);
    }

    /**
     * Ajoute un <i>listener</i> pour le joueur en cours
     * 
     * @param pl
     *            <i>Listener</i> pour le joueur en cours
     */
    public void addPlayerListener(PlayerListener pl) {
        this.playerListeners.add(PlayerListener.class, pl);
    }

    /**
     * Ajoute un <i>listener</i> pour le rack du joueur en cours
     * 
     * @param rl
     *            <i>Listener</i> du rack
     */
    public void addRackListener(RackListener rl) {
        this.getCurrentPlayer().getRack().addRackListener(rl);
        fireRackListeners();
    }

    /**
     * Ajoute un <i>listener</i> pour les évènements de tour
     * 
     * @param tl
     *            <i>Listener</i> pour les tours
     */
    public void addTurnListener(TurnListener tl) {
        this.turnListeners.add(TurnListener.class, tl);
    }

    /**
     * Termine le jeu. Cette méthode tient compte des règles spéciales de la fin
     * du jeu de Scrabble.
     * <ul>
     * <li>À la fin de la partie, on retranche au score de tous les joueurs la
     * somme des points qu'il leur reste sur le rack.</li>
     * <li>Si un joueur termine la partie avec plus aucune lettre, on ajoute
     * cette somme de points de tous les joueurs au score de celui-ci.</li>
     * </ul>
     */
    public void endGame() {
        int bonusPremierFinir = 0;
        Player premierFinir = null;
        for (Player p : players) {
            bonusPremierFinir += p.endGame();
            if (p.getRack().isEmpty())
                premierFinir = p;
        }

        if (premierFinir != null)
            premierFinir.incScore(bonusPremierFinir);
    }

    public void firePlayerListeners() {
        PlayerEvent pe = new PlayerEvent(this, currentPlayer);
        for (PlayerListener pl : playerListeners
                .getListeners(PlayerListener.class)) {
            pl.playerChanged(pe);
        }
    }

    private void fireRackListeners() {
        getCurrentPlayer().getRack().fireRackListeners();
    }

    private void fireTurnListeners(TurnEvent turnEvent) {
        for (TurnListener tl : turnListeners.getListeners(TurnListener.class)) {
            tl.turnPlayed(turnEvent);
        }
    }

    /**
     * @return Le sac utilisé dans cette partie
     */
    public Bag getBag() {
        return bag;
    }

    /**
     * @return Le plateau de jeu
     */
    public Board getBoard() {
        return board;
    }

    /**
     * @return Le plateau de jeu
     */
    public IBoard getIBoard() {
        return board;
    }

    /**
     * @return Le joueur en cours
     */
    public Player getCurrentPlayer() {
        return this.players.get(currentPlayer);
    }

    /**
     * @return Liste des joueurs participant
     */
    public List<Player> getPlayers() {
        return players;
    }

    public List<IPlayer> getIPlayers() {
        ArrayList<IPlayer> iplayers = new ArrayList<IPlayer>();
        iplayers.addAll(players);
        return iplayers;
    }

    private RackListener[] getRackListeners() {
        return this.getCurrentPlayer().getRack().getRackListeners();
    }

    /**
     * Détermine si le jeu est terminé en fonction de deux critères :
     * <ul>
     * <li>Un joueur n'a plus de lettres</li>
     * <li>Tous les joueurs ont passé dans le même tour</li>
     * </ul>
     * 
     * @return <code>true</code> si le jeu est terminé, <code>false</code>
     *         sinon.
     */
    public boolean isEnded() {
        int remainingPlayers = 0;
        for (IPlayer p : players) {
            if (p.getRack().isEmpty())
                return true;
            if (!p.hasSkip())
                remainingPlayers++;
        }
        return remainingPlayers < 1;
    }

    /**
     * Passe au joueur suivant
     */
    public void nextPlayer() {

        RackListener[] listeners = getRackListeners();
        for (RackListener rl : listeners) {
            removeRackListener(rl);
        }
        getCurrentPlayer().getRack().fill(bag);
        if (isEnded()) {
            fireErrorListeners(new EndGameError());
            return;
        }
        currentPlayer++;
        currentPlayer %= players.size();
        getCurrentPlayer().setSkipped(false);
        firePlayerListeners();

        for (RackListener rl : listeners) {
            addRackListener(rl);
        }
        fireRackListeners();
    }

    /**
     * Joue un coup en posant les lettres spécifiées
     * 
     * @param letters
     *            Table d'association entre les tuiles et leurs coordonées sur
     *            le plateau
     * @return <code>true</code> si le coup c'est bien passé, <code>false</code>
     *         sinon.
     */
    public boolean play(HashMap<Coordinate, Tile> letters) {
        if (letters.isEmpty()) {
            fireErrorListeners(new BadMoveError(Resources
                    .getString("Error.NotWord")));
            return false;
        }

        if (!getCurrentPlayer().getRack().getTiles().containsAll(
                letters.values())) {
            fireErrorListeners(new BadMoveError(Resources
                    .getString("Error.ForbiddenTiles")));
            return false;
        }

        // Étape 1 : On teste si le mot est bien dans une seule direction
        int max_l = Integer.MIN_VALUE;
        int max_c = Integer.MIN_VALUE;

        int min_l = Integer.MAX_VALUE;
        int min_c = Integer.MAX_VALUE;

        boolean estAuMilieu = false;
        for (Coordinate coord : letters.keySet()) {
            if (board.hasTile(coord)) {
                fireErrorListeners(new BadMoveError(Resources.getString(
                        "Error.TileAlreadyHere", coord)));
                return false;
            }
            max_c = Math.max(coord.getColonne(), max_c);
            max_l = Math.max(coord.getLigne(), max_l);
            min_c = Math.min(coord.getColonne(), min_c);
            min_l = Math.min(coord.getLigne(), min_l);
            if (board.getBonus(coord) == Bonus.STAR)
                estAuMilieu = true;
        }

        // On vérifie si c'est le premier mot, et si il touche la case centrale
        // ou non.
        if (!estAuMilieu && wordCount == 0) {
            fireErrorListeners(new BadMoveError(Resources.getString("Error.FirstWordStar")));
            return false;
        }

        boolean vertical = (max_c == min_c);
        boolean horizontal = (max_l == min_l);

        if (!horizontal && !vertical) {
            fireErrorListeners(new BadMoveError(Resources.getString("Error.WordAlignment")));
            return false;
        }

        // A partir de là, si on a horizontal == vertical (== true), c'est qu'il
        // s'agit d'une seule lettre posée.

        // On teste si le mot est bien rempli
        if (horizontal) {
            for (int x = min_l; x <= max_l; x++) {
                Coordinate coord = new Coordinate(x, min_c);
                if (!letters.keySet().contains(coord)
                        && !this.board.hasTile(coord)) {
                    fireErrorListeners(new BadMoveError(Resources.getString("Error.ContiguousWord")));
                    return false;
                }
            }
        }

        if (vertical) {
            for (int y = min_c; y <= max_c; y++) {
                Coordinate coord = new Coordinate(min_l, y);
                if (!letters.keySet().contains(coord)
                        && !this.board.hasTile(coord)) {
                    fireErrorListeners(new BadMoveError(Resources.getString("Error.ContiguousWord")));
                    return false;
                }
            }
        }

        // On teste si le mot touche un autre ou l'étoile centrale
        boolean trouve = false;
        Iterator<Coordinate> lettresPlacees = letters.keySet().iterator();

        while (!trouve && lettresPlacees.hasNext()) {
            Coordinate square = lettresPlacees.next();
            if (square.equals(START_SQUARE))
                trouve = true;
            Iterator<Coordinate> casesVoisines = square.aroundCoordinates()
                    .iterator();
            while (!trouve && casesVoisines.hasNext()) {
                Coordinate voisin = casesVoisines.next();
                if (board.hasTile(voisin))
                    trouve = true;
            }
        }

        if (!trouve) {
            fireErrorListeners(new BadMoveError(Resources.getString("Error.NoLinkWithOtherWords")));
            return false;
        }
        String motPlace = "";
        // On regarde les mots générés avec ce placement
        Set<String> words = new HashSet<String>();
        if (vertical) {

            StringBuffer motPrincipal = new StringBuffer();
            int start_line = min_l;
            while (start_line > 0 && board.hasTile(start_line - 1, min_c)) {
                start_line--;
            }

            int end_line = max_l;
            while (end_line + 1 < BOARD_HEIGHT
                    && board.hasTile(end_line + 1, min_c)) {
                end_line++;
            }
            for (int i = start_line; i <= end_line; i++) {
                if (board.hasTile(i, min_c)) {
                    ITile tile = board.getTile(i, min_c);
                    motPrincipal.append(tile.getLetter());
                } else {
                    Coordinate coord = new Coordinate(i, min_c);

                    motPrincipal.append(letters.get(coord).getLetter());
                }
            }
            if (motPrincipal.toString().length() > 1) {

                motPlace = motPrincipal.toString();
                words.add(motPrincipal.toString());
            }

            for (int i = min_l; i <= max_l; i++) {
                Coordinate location = new Coordinate(i, min_c);
                String mot = board.wordToWest(new Coordinate(i, min_c))
                        + (letters.get(location) == null ? board
                                .getTile(location) : letters.get(location))
                                .getLetter()
                        + board.wordToEast(new Coordinate(i, min_c));
                if (mot.length() > 1)
                    words.add(mot);
            }
        }

        if (horizontal) {
            StringBuffer motPrincipal = new StringBuffer();
            int start_col = min_c;
            while (start_col > 0 && board.hasTile(min_l, start_col - 1)) {
                start_col--;
            }

            int end_col = max_c;
            while (end_col + 1 < BOARD_WIDTH
                    && board.hasTile(min_l, end_col + 1)) {
                end_col++;
            }
            for (int i = start_col; i <= end_col; i++) {
                if (board.hasTile(min_l, i)) {
                    ITile tile = board.getTile(min_l, i);
                    motPrincipal.append(tile.getLetter());
                } else {
                    Coordinate coord = new Coordinate(min_l, i);

                    motPrincipal.append(letters.get(coord).getLetter());
                }
            }
            if (motPrincipal.toString().length() > 1) {
                motPlace = motPrincipal.toString();
                words.add(motPrincipal.toString());
            }

            for (int i = min_c; i <= max_c; i++) {
                Coordinate location = new Coordinate(min_l, i);
                String mot = board.wordToNorth(new Coordinate(min_l, i))
                        + (letters.get(location) == null ? board
                                .getTile(location) : letters.get(location))
                                .getLetter()
                        + board.wordToSouth(new Coordinate(min_l, i));
                if (mot.length() > 1)
                    words.add(mot);
            }
        }

        if (words.isEmpty()) {
            fireErrorListeners(new BadMoveError(Resources.getString("Error.NotWord")));
            return false;
        }
        for (String mot : words) {
            if (!Dictionary.getInstance().contains(mot)) {
                fireErrorListeners(new BadMoveError(Resources.getString(
                        "Dictionary.WordNotFound", mot)));
                return false;
            }
        }

        fireTurnListeners(new TurnEvent(this, getCurrentPlayer(), motPlace,
                Collections.min(letters.keySet())));
        int score = board.put(letters, horizontal);
        getCurrentPlayer().getRack().removeAll(letters.values());
        getCurrentPlayer().incScore(score);
        wordCount++;
        return true;
    }

    /**
     * Supprime un <i>listener</i> sur le joueur en cours
     * 
     * @param pl
     *            Le <i>listener</i> à supprimer.
     */
    public void removePlayerListener(PlayerListener pl) {
        this.playerListeners.remove(PlayerListener.class, pl);
    }

    /**
     * Supprime un <i>listener</i> sur le rack du joueur en cours
     * 
     * @param rl
     *            Le <i>listener</i> à supprimer
     */
    public void removeRackListener(RackListener rl) {
        this.getCurrentPlayer().getRack().removeRackListener(rl);
    }

    /**
     * Supprime un <i>listener</i> sur les coups joués
     * 
     * @param tl
     *            Le <i>listener</i> à supprimer
     */
    public void removeTurnListener(TurnListener tl) {
        this.turnListeners.remove(TurnListener.class, tl);
    }

    /**
     * Le joueur en cours passe son tour
     */
    public void skipTurn() {
        skipTurn(true);
    }

    /**
     * Le joueur en cours passe son tour
     * 
     * @param reallySkip
     *            Détermine si le saut de joueur compte pour la fin du jeu.
     */
    public void skipTurn(boolean reallySkip) {
        if (reallySkip) {
            fireTurnListeners(new TurnEvent(this, getCurrentPlayer(),
                    TurnEvent.SKIP));
            getCurrentPlayer().setSkipped(true);
        }
        nextPlayer();
    }

    /**
     * Démarre la partie en distribuant les lettres aux joueurs et en mélangeant
     * la liste des joueurs.
     */
    public void start() {
        Collections.shuffle(players);
        for (IPlayer p : players) {
            p.getRack().fill(bag);
        }
        RackListener[] listeners = getRackListeners();
        for (RackListener rl : listeners) {
            removeRackListener(rl);
        }
        currentPlayer = 0;
        firePlayerListeners();
        for (RackListener rl : listeners) {
            addRackListener(rl);
        }
        fireRackListeners();
    }

    /**
     * Le joueur en cours échange des tuiles
     * 
     * @param tilesToSwap
     *            Les tuiles que le joueur veut échanger
     */
    public void swapTiles(List<Tile> tilesToSwap) {
        if (tilesToSwap.isEmpty()) {
            return;
        }
        ArrayList<Letter> letters = new ArrayList<Letter>();
        for (ITile t : tilesToSwap) {
            letters.add(t.getLetter());
        }
        if (!getCurrentPlayer().getRack().containsAll(letters)) {
            fireErrorListeners(new ForbiddenActionError(Resources.getString("Error.ForbiddenTiles")));
            return;
        }

        if (bag.size() < tilesToSwap.size()) {
            fireErrorListeners(new EmptyBagError(Resources.getString("Error.ForbiddenTiles")));
            return;
        }

        fireTurnListeners(new TurnEvent(this, getCurrentPlayer(),
                TurnEvent.SWAP));
        tilesToSwap = getCurrentPlayer().getRack().takeAll(letters);
        getCurrentPlayer().getRack().fill(bag);
        for (Tile tile : tilesToSwap) {
            bag.put(tile);
        }

    }

    public int getWordCount() {
        return this.wordCount;
    }

    public int incWordCount() {
        return ++this.wordCount;
    }

    public void addErrorListener(ErrorListener errorListener) {
        errorListeners.add(ErrorListener.class, errorListener);
    }

    private void fireErrorListeners(ErrorEvent errorEvent) {
        for (ErrorListener tl : errorListeners
                .getListeners(ErrorListener.class)) {
            tl.errorOccured(errorEvent);
        }
    }

}
